문제 소개
문제 링크 : https://boj.kr/3654
Tier : Diamond IV
Tag : 2-SAT
3칸을 차지하는 L모양의 퍼즐 조각이 있다. 중간은 검은 색이며, 나머지 두 칸은 하얀 색이다.
각 테스트 케이스마다 N * M 크기의 패턴이 주어지는데,
우리는 해당 퍼즐 조각을 적절히 놓았을 때 패턴을 온전히 만들 수 있는지에 대한 여부를 확인해야 한다.
이때, 퍼즐 조각은 회전 가능하지만 겹칠 수 없다.
풀이
1. 일반적인 규칙 찾기
패턴 내의
-
검은 색 칸을 기준으로 가로에 하얀 색 칸 하나, 세로에 하얀 색 칸 하나가 붙어 있어야 L모양이 성립한다. (a)
-
하얀 색 칸을 기준으로 인접하는 검은 색 칸 중 하나에만 포함되어야 한다. (b)
2. 유효하지 않은 케이스 찾기
패턴 내의
-
하얀 색 칸 개수가 검은 색 칸 개수의 정확히 두 배여야 한다. (c)
-
하얀 색 칸과 인접한 칸 중 검은 색 칸이 없으면 유효하지 않은 패턴이다. (d)
-
검은 색 칸 기준으로 각각 가로, 세로에 인접한 칸 중 하나라도 하얀 색 칸이 없으면 유효하지 않은 패턴이다. (e)
3. 위의 규칙을 토대로 2-SAT으로 모델링하기
어떻게 하면 이 규칙을 논리식으로 변환할 수 있을까?
특정 변수 a, b, c, ...에 대해,
- (a v b) : a 또는 b가 true여야 성립
- (a v a) : a가 true여야 성립
- (!a v !b) ^ (!a v !c) ^ ... : a, b, c, ... 중 1개 이하만 true여야 성립
- (!a v b) ^ (a v !b) ^ (!a v c) ^ (a v !c) ^ ... : a, b, c, ... 모두 true이거나 false여야 성립
등의 방식으로 논리식을 모델링할 수 있다.
맹점 1
이를 이용하여 **규칙 (b)**의 일부를 충족하지만, 검은 색 칸 전부 포함되지 않는 경우도 생길 수 있다.
그러나, 이 맹점은 규칙 (c)를 통해 해결할 수 있다.
왜냐하면, 저런 상황이 올 경우 하얀 색 칸이 남게 되기 때문이다.
맹점 2
특정한 검은 색 칸에 대해, 주변의 하얀 색 칸이 가로 세로당 하나씩만 붙어야 하는 **조건 (a)**는 어떻게 충족시켜야 할까?
이러한 문제점을 다음과 같은 변수 설정으로 해결할 수 있다.
검은 색 칸에 인접한 가로 칸 중, 왼쪽에 존재하는 하얀 칸을 false, 오른쪽에 존재하는 하얀 칸을 true로 간주하여 변수를 설정한다.
세로 칸의 경우에도 마찬가지이다.
특정 변수는 무조건 true 또는 false이므로, 논리식을 이용하지 않고 변수 설정만으로 해결할 수 있다.
나머지 규칙 (d), (e)의 경우 특정 칸에 겹치는 변수를 모두 몰아넣어 (b)를 구성할 때 같이 체크하면 된다.
코드
1#include <bits/stdc++.h>2#define FAST_IO ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr)34#define BLANK 05#define BLACK 16#define WHITE 27#define MAX 101010189using namespace std;1011vector<int> nodes[502][502], graph[MAX];12stack<int> candidate;1314int discovered[MAX], sccIDs[MAX];15int puzzle[502][502];1617int nodeID, sccID;18int N, M, whiteID;19bool possible = true;2021// 테스트 케이스마다 초기화한다.22void init() {23for (int i = 0; i < 502; i++) {24for (int j = 0; j < 502; j++) {25puzzle[i][j] = BLANK;26nodes[i][j] = vector<int>();27}28}2930for (int i = 0; i < MAX; i++) {31graph[i] = vector<int>();32}3334candidate = stack<int>();35possible = true;3637nodeID = sccID = 1;38whiteID = 0;3940memset(discovered, 0, sizeof discovered);41memset(sccIDs, 0, sizeof sccIDs);42}4344// SCC를 형성한다.45int createSCC(int node) {46int id = discovered[node] = nodeID++;47candidate.push(node);4849for (auto next : graph[node]) {50if (!discovered[next])51id = min(id, createSCC(next));5253else if (!sccIDs[next])54id = min(id, discovered[next]);55}5657if (id == discovered[node]) {58while (true) {59int top = candidate.top();60candidate.pop();6162sccIDs[top] = sccID;6364if (top == node)65break;66}6768sccID++;69}7071return id;72}7374// 패턴 매치가 가능한 지 체크한다.75void check() {76for (int i = 0; i < whiteID; i++) {77if (!discovered[i]) createSCC(i);78}7980for (int i = 0; i < whiteID; i += 4) {81int left = i, right = i + 1, top = i + 2, bottom = i + 3;8283if ((sccIDs[left] == sccIDs[right]) || (sccIDs[top] == sccIDs[bottom])) {84possible = false;85return;86}87}88}8990// 패턴 정보를 토대로 2-SAT 그래프를 모델링한다.91void analyse() {92// 1. B 기준93for (int i = 1; i <= N; i++) {94for (int j = 1; j <= M; j++) {95if (puzzle[i][j] != BLACK) continue;9697// 1-1. Horizontal98int left = whiteID, right = whiteID + 1;99100if (puzzle[i][j - 1] == WHITE && puzzle[i][j + 1] == WHITE) {101nodes[i][j - 1].push_back(left);102nodes[i][j + 1].push_back(right);103}104105else if (puzzle[i][j - 1] != WHITE && puzzle[i][j + 1] == WHITE) {106nodes[i][j + 1].push_back(right);107108// 오른쪽 W가 무조건 들어가야 한다.109graph[left].push_back(right);110}111112else if (puzzle[i][j - 1] == WHITE && puzzle[i][j + 1] != WHITE) {113nodes[i][j - 1].push_back(left);114115// 왼쪽 W가 무조건 들어가야 한다.116graph[right].push_back(left);117}118119// 둘 다 불가능한 경우 L퍼즐을 만들 수 없다.120else {121possible = false;122return;123}124125// 1-2. Vertical126int top = whiteID + 2, bottom = whiteID + 3;127128if (puzzle[i - 1][j] == WHITE && puzzle[i + 1][j] == WHITE) {129nodes[i - 1][j].push_back(top);130nodes[i + 1][j].push_back(bottom);131}132133else if (puzzle[i - 1][j] != WHITE && puzzle[i + 1][j] == WHITE) {134nodes[i + 1][j].push_back(bottom);135136// 아래쪽 W가 무조건 들어가야 한다.137graph[top].push_back(bottom);138}139140else if (puzzle[i - 1][j] == WHITE && puzzle[i + 1][j] != WHITE) {141nodes[i - 1][j].push_back(top);142143// 위쪽 W가 무조건 들어가야 한다.144graph[bottom].push_back(top);145}146147// 둘 다 불가능한 경우 L퍼즐을 만들 수 없다.148else {149possible = false;150return;151}152153whiteID += 4;154}155}156157// 2. W 기준158for (int i = 1; i <= N; i++) {159for (int j = 1; j <= M; j++) {160if (puzzle[i][j] != WHITE) continue;161162// W가 어떤 B에도 인접하지 않은 경우 L퍼즐을 만들 수 없다.163if (nodes[i][j].empty()) {164possible = false;165return;166}167168int U = (int) nodes[i][j].size();169170// 2-1. B가 하나인 경우, 이 W는 해당 B를 무조건 선택해야 한다.171if (U == 1) {172int v = nodes[i][j][0];173graph[v ^ 1].push_back(v);174175continue;176}177178// 2-2. 이들 중 두 개 이상을 만족시키지 않도록 한다.179for (int x = 0; x < U - 1; x++) {180int vx = nodes[i][j][x];181182for (int y = x + 1; y < U; y++) {183int vy = nodes[i][j][y];184185// (!x1 v !y1) = y1 -> x2, x1 -> y2186graph[vy].push_back(vx ^ 1);187graph[vx].push_back(vy ^ 1);188}189}190}191}192}193194int main() {195FAST_IO;196int T; cin >> T;197198while (T--) {199init();200201// 1. 입력 받기202cin >> N >> M;203204int white = 0, black = 0;205206for (int i = 1; i <= N; i++) {207for (int j = 1; j <= M; j++) {208char c; cin >> c;209210if (c == 'B') { black++; puzzle[i][j] = BLACK; }211if (c == 'W') { white++; puzzle[i][j] = WHITE; }212if (c == '.') { puzzle[i][j] = BLANK; }213}214}215216if (white != black << 1) {217cout << "NO" << "\n";218continue;219}220221// 2. 패턴을 그래프화222analyse();223224if (!possible) {225cout << "NO" << "\n";226continue;227}228229// 3. 패턴 매칭 체크230check();231232cout << (possible ? "YES" : "NO") << "\n";233}234235return 0;236}237